给 K8s 中的 Operator 添加 Webhook 功能【保姆级】
❝本文转自油腻中年李大鹅,原文:https://juejin.cn/post/7132752189646667784,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang
简介
准入控制器是在对象持久化之前用于对 Kubernetes API Server 的请求进行拦截的代码段,在请求经过身份验证和授权之后放行通过。准入控制器可能正在 validating、mutating 或者都在执行,Mutating 控制器可以修改他们处理的资源对象,Validating 控制器不会,如果任何一个阶段中的任何控制器拒绝了请求,则会立即拒绝整个请求,并将错误返回给最终的用户。
时序图如下所示:
如果我们想为 CRD 实现 admission webhook,我们唯一要做的就是实现 Defaulter
和 (或)Validator
接口。
Kubebuilder 会我们处理剩余的工作:
创建 webhook 服务器 确保服务器已添加到管理器中 为 webhook 创建处理程序 使用服务器中的路径注册每个处理程序
设计实战场景
这里我们直接在手摸手教你使用 kubebuilder 开发 operator[1]中实战的 app-operator 项目上来扩展功能。
这里我们扩展两个功能:
1、假设用户的 CR 中没有设置总 QPS,我们可以通过 webhook 来设置一个默认值,比如 1300;
2、为了保护系统,给单个 Pod 的 QPS 设置上限,比如 1000,如果用户的 CR 中 singlePodsQPS 的值超过 1000,webhook 进行拦截,创建资源对象失败。
部署证书管理器
和 controller 类似,webhook 既可以在 kubernetes 环境中运行,也可以在 kubernetes 环境外运行,如果 webhook 在 kuberneetes 环境之外运行,需要将证书放在所在的环境中,默认路径:
/tmp/k8s-webhook-server/serving-certs/tls.{crt,key}
这里选择是将 webhook 部署在 kubernetes 环境中,使用cert-manager[2]
来管理证书。
直接使用下面命令安装 cert-manager 组件即可:
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml
命令执行成功后,会自动创建 cert-manager 命名空间,以及 rbac、pod、service 等资源:
$ kubectl get all -n cert-manager
NAME READY STATUS RESTARTS AGE
pod/cert-manager-6544c44c6b-gf6nk 1/1 Running 0 20h
pod/cert-manager-cainjector-5687864d5f-22dzw 1/1 Running 0 20h
pod/cert-manager-webhook-785bb86798-nh2gc 1/1 Running 0 20h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/cert-manager ClusterIP 10.109.226.106 <none> 9402/TCP 20h
service/cert-manager-webhook ClusterIP 10.106.116.190 <none> 443/TCP 20h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/cert-manager 1/1 1 1 20h
deployment.apps/cert-manager-cainjector 1/1 1 1 20h
deployment.apps/cert-manager-webhook 1/1 1 1 20h
NAME DESIRED CURRENT READY AGE
replicaset.apps/cert-manager-6544c44c6b 1 1 1 20h
replicaset.apps/cert-manager-cainjector-5687864d5f 1 1 1 20h
replicaset.apps/cert-manager-webhook-785bb86798 1 1 1 20h
开发 webhook
进入 app-operator 工程下,在终端执行以下命令创建 webhook:
$ kubebuilder create webhook \
--group elasticweb \
--version v1 \
--kind ElasticWeb \
--defaulting \
--programmatic-validation
上述命令执行完毕,kubebuilder 会为我们创建 webhook 的处理程序。
首先来查看 main.go
文件,不难发现,kuberbuilder 为我们新增了如下代码:
if err = (&elasticwebv1.ElasticWeb{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ElasticWeb")
os.Exit(1)
}
同时不难发现,在 api/v1
下,新增了 elasticweb_webhook.go
和 webhook_suite_test.go
两个文件,同时在 config
目录下,也新增了 webhook 的相关配置。
这里我们最为关心的就是 elasticweb_webhook.go
这个文件了,因为我们的主要逻辑都是要在这里实现。
配置
在进行编写业务逻辑之前,我们需要对 config/default/kustomization.yaml
文件做一些修改,原文件中对 webhook 的配置部分都是注释掉的,我们要做的是就是启用这些配置。
这些配置分别是- ../webhook
,- ../certmanager
,- manager_webhook_patch.yaml
,- webhookcainjection_patch.yaml
以及 vars
下的全部内容。
# config/default/kustomization.yaml
# Adds namespace to all resources.
namespace: elasticweb-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: elasticweb-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
patchesStrategicMerge:
# Protect the /metrics endpoint by putting it behind auth.
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, please comment the following line.
- manager_auth_proxy_patch.yaml
# Mount the controller config file for loading manager configurations
# through a ComponentConfig type
#- manager_config_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- manager_webhook_patch.yaml
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
- webhookcainjection_patch.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATE_NAME
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: SERVICE_NAME
objref:
kind: Service
version: v1
name: webhook-service
编码
一切就绪后,现在开始实现业务逻辑。
打开 api/v1/elasticweb_webhook.go
文件。
新增依赖:
apierrors "k8s.io/apimachinery/pkg/api/errors"
首先我们来实现 Default
方法。
Default 就是来判断 CR 中的 TotalQPS 的值是否为空,如果为空值,那么就给它设置个默认值:
// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *ElasticWeb) Default() {
elasticweblog.Info("default", "name", r.Name)
// TODO(user): fill in your defaulting logic.
// 如果创建的时候没有输入总QPS,就设置个默认值
if r.Spec.TotalQPS == nil {
r.Spec.TotalQPS = new(int32)
*r.Spec.TotalQPS = 1300
elasticweblog.Info("a. TotalQPS is nil,set default value now", "totalQPS", *r.Spec.TotalQPS)
} else {
elasticweblog.Info("b. TotalQPS exists", "TotalQPS", r.Spec.TotalQPS)
}
}
接下来我们实现验证功能,由于这里我们需要验证 Create
和 Update
, 它们使用同一套逻辑,为了代码简洁以及复用性,我们封装一个 validateElasticWeb 方法。
我们主要是验证 singlePodsQPS 是否大于 1000,如果是,则直接拦截。
func (r *ElasticWeb) validateElasticWeb() error {
var allErrs field.ErrorList
if *r.Spec.SinglePodsQPS > 1000 {
elasticweblog.Info("c. Invalid SinglePodQPS")
err := field.Invalid(field.NewPath("spec").Child("singlePodQPS"),
*r.Spec.SinglePodsQPS,
"d. must be less than 1000")
allErrs = append(allErrs, err)
return apierrors.NewInvalid(
schema.GroupKind{
Group: "elasticweb.example.com",
Kind: "ElasticWeb",
},
r.Name,
allErrs)
} else {
elasticweblog.Info("e. SinglePodQPS is valid")
return nil
}
}
通过上面的代码可见,最终是调用 apierrors.NewInvalid
生成错误实例,而此方法接收的是多个错误,因此需要为期准备切片作为入参,如果是多个参数校验失败,可以直接放入切片。
最终我们需要在 ValidateCreate()
和 ValidateUpdate()
方法中调用我们上面封装的 validateElasticWeb
。
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
elasticweblog.Info("validate create", "name", r.Name)
// TODO(user): fill in your validation logic upon object creation.
return r.validateElasticWeb()
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
elasticweblog.Info("validate update", "name", r.Name)
// TODO(user): fill in your validation logic upon object update.
return r.validateElasticWeb()
}
测试
部署
由于我们选择使用 cert-manager 来管理证书,所以我们测试的时候需要将 operator 部署到集群。
1、部署 CRD:
$ make install
2、构建镜像并推送到镜像仓库
$ make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.2
3、部署集成了 webhook 功能的 controller
$ make deploy IMG=huiyichanmian/elasitcweb:v0.0.2
4、查看,确保启动成功
$ kubectl get pod -n elasticweb-system
NAME READY STATUS RESTARTS AGE
elasticweb-controller-manager-5dc874656b-dsqt5 2/2 Running 0 4h41m
验证 Defaulter
修改 config/samples/elasticweb_v1_elasticweb.yaml
, 将 totalQPS
字段进行注释,完整文件如下所示:
apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
namespace: dev
spec:
image: nginx:1.17.1
port: 30003
singlePodsQPS: 800
# totalQPS: 2400
此时我们设置的单个 Pod 的 QPS 是 800,如果 webhook 生效,总 QPS 就是 1500,那么对应的 Pod 应该是两个,我们直接创建上面的资源对象后,查看是否符合我们的预期。
$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
elasticweb.elasticweb.example.com/elasticweb-sample created
$ kubectl get pod -n dev
NAME READY STATUS RESTARTS AGE
elasticweb-sample-566d5dd6d9-6dzdj 1/1 Running 0 7s
elasticweb-sample-566d5dd6d9-6hqjd 1/1 Running 0 7s
可见此时的 Pod 的数量是 2。符合我们的预期。
接着使用 kubectl describe
命令来查看 elasticweb 资源对象的详情。
$ kubectl describe elasticweb elasticweb-sample -n dev
Name: elasticweb-sample
Namespace: dev
Labels: <none>
Annotations: <none>
API Version: elasticweb.example.com/v1
Kind: ElasticWeb
Metadata:
Creation Timestamp: 2022-08-17T07:18:25Z
Generation: 1
......
Spec:
Image: nginx:1.17.1
Port: 30003
Single Pods QPS: 800
Total QPS: 1300
Events: <none>
可以看到 Total QPS 字段被 webhook 设置为 1300,RealQPS 也计算正确。
再来看 Controller 的日志。
其中的 webhook 部分是否符合预期,如上图红框所示,发现 TotalQPS 字段为空,就将设置为默认值,并且在检测的时候 SinglePodQPS 的值也没有超过 1000。
验证 Validator
接下来就是验证 webhook 的参数校验功能了,其实创建时的逻辑已经在上面验证过了,我们现在的 singlePodsQPS 为 800,小于 1000,符合要求,从 controller 日志中可以看到 "e. SinglePodQPS is valid"。
那么我们直接修改 config/samples/elasticweb_v1_elasticweb.yaml
中 singlePodsQPS 的值,将 800 改为 1200。
$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
The ElasticWeb "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1200: d. must be less than 1000
发现直接在终端抛出了错误。
使用 kubectl describe
命令来查看 elasticweb 资源对象的详情,可以发现 Single Pods QPS 的值依然是 800。
查看日志:
如何在本地测试
1、获取证书:
$ kubectl get secrets webhook-server-cert -n elasticweb-system -o jsonpath='{..tls\.crt}' |base64 -d > certs/tls.crt\n
$ kubectl get secrets webhook-server-cert -n elasticweb-system -o jsonpath='{..tls\.key}' |base64 -d > certs/tls.key
2、修改 main.go
, 让 webhook server 使用指定证书:
if os.Getenv("ENVIRONMENT") == "DEV" {
path, err := os.Getwd()
if err != nil {
setupLog.Error(err, "unable to get work dir")
os.Exit(1)
}
options.CertDir = path + "/certs"
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
3、运行
$ make run ENVIRONMENT=DEV
补充
controller 有两种运行方式,一种是在 kubernetes 环境内,一种是在 kubernetes 环境外独立运行,在编码阶段我们通常会在开发环境上运行 controller,但是如果使用了 webhook, 由于其特殊的鉴权方式,需要将 kubernetes 签发的证书放置在本地的 tmp/k8s-webhook-server/serving-crets/
目录。
面对这种问题,官方给出的建议是:如果在开发阶段暂时用不到 webhook,那么
在本地运行 controller 时屏蔽 webhook 的功能。
具体操作是首先修改 main.go
文件,其实就是在 webhook 控制器这块添加一个环境变量的判断 :
if os.Getenv("ENABLE_WEBHOOK") != "false" {
if err = (&elasticwebv1.ElasticWeb{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ElasticWeb")
os.Exit(1)
}
}
在本地启动 controller 得时候,使用 make run ENABLE_WEBHOOK=false
即可。
引用链接
[1]手摸手教你使用 kubebuilder 开发 operator: https://juejin.cn/post/7099354856078442509
[2]cert-manager: https://link.juejin.cn?target=https%3A%2F%2Fcert-manager.io%2F
你可能还喜欢
点击下方图片即可阅读
2022-09-06
2022-09-05
2022-09-04
2022-08-31
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?